package com.yvan.demo.mvc;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.yvan.YvanUtil;
import com.yvan.platform.ReflectUtil;
import io.swagger.annotations.ApiModelProperty;
import lombok.Cleanup;
import lombok.SneakyThrows;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.core.Conventions;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;

public class JsonBodyResolver implements HandlerMethodArgumentResolver {

    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.getParameterAnnotation(JsonBody.class) != null;
    }

    /**
     * 见 org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor:131 行
     */
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
                                  WebDataBinderFactory binderFactory) throws Exception {
        JsonBody attr = parameter.getParameterAnnotation(JsonBody.class);

        Object requestObj = webRequest.getNativeRequest();
        if (!(requestObj instanceof HttpServletRequest)) {
            throw new IllegalStateException("not HttpServletRequest");
        }

        HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
        @Cleanup InputStream is = request.getInputStream();
        Object arg = YvanUtil.objectMapper.readValue(is, parameter.getParameterType());
        String name = Conventions.getVariableNameForParameter(parameter);

        //见 org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor:131 行
        WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
        if (arg != null) {
            validateIfApplicable(binder, parameter);
            if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
                throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
            }
        }
        mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());

        return arg;
    }

    @SneakyThrows
    private void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
        Annotation[] annotations = parameter.getParameterAnnotations();
        for (Annotation ann : annotations) {
            Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
            if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
                Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
                Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[]{hints});
                binder.validate(validationHints);
                break;
            }
        }

        // mvc validate 完毕之后，针对字段名，给出提示
        Class<?> targetClass = binder.getTarget().getClass();

        BindingResult bindingResult = binder.getBindingResult();
        for (ObjectError objectError : bindingResult.getAllErrors()) {
            if (objectError instanceof FieldError) {
                //字段错误, 找到类对应的标注
                FieldError error = (FieldError) objectError;

                Field field = targetClass.getDeclaredField(error.getField());

                //在描述(defaultMessage)中增加字段名
                ApiModelProperty apiModelProperty = AnnotationUtils.findAnnotation(field, ApiModelProperty.class);
                if (apiModelProperty != null) {
                    ReflectUtil.setValue(DefaultMessageSourceResolvable.class, error, "defaultMessage",
                            "[" + apiModelProperty.value() + "] " + error.getDefaultMessage());
                }

                //把对象名称(objectName)改为json属性名
                JsonProperty jsonProperty = AnnotationUtils.findAnnotation(field, JsonProperty.class);
                if (jsonProperty != null) {
                    ReflectUtil.setValue(ObjectError.class, error, "objectName",
                            jsonProperty.value());
                } else {
                    ReflectUtil.setValue(ObjectError.class, error, "objectName",
                            error.getField());
                }
            }
        }
    }

    private boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter parameter) {
        int i = parameter.getParameterIndex();
        Class<?>[] paramTypes = parameter.getMethod().getParameterTypes();
        boolean hasBindingResult = (paramTypes.length > (i + 1) && Errors.class.isAssignableFrom(paramTypes[i + 1]));
        return !hasBindingResult;
    }

    public static byte[] readBytes(InputStream is, int contentLen) {
        if (contentLen > 0) {
            int readLen = 0;
            int readLengthThisTime = 0;
            byte[] message = new byte[contentLen];
            try {
                while (readLen != contentLen) {
                    readLengthThisTime = is.read(message, readLen, contentLen - readLen);
                    if (readLengthThisTime == -1) {// Should not happen.
                        break;
                    }
                    readLen += readLengthThisTime;
                }
                return message;
            } catch (IOException e) {
            }
        }
        return new byte[]{};
    }
}
